import random
import re
from typing import List, Optional, Tuple

BoardType = List[List[Optional[int]]]
CoordinateType = Tuple[int, int]

BOARD_WIDTH = 10
BOARD_HEIGHT = 10


# data structure keeping track of information
# about the ships in the game. for each ship,
# the following information is provided:
#
#   name - string representation of the ship
#   length - number of "parts" on the ship that
#            can be shot
#   shots - number of shots the ship counts for
SHIPS = [
    ("BATTLESHIP", 5, 3),
    ("CRUISER", 3, 2),
    ("DESTROYER<A>", 2, 1),
    ("DESTROYER<B>", 2, 1),
]

VALID_MOVES = [
    [-1, 0],  # North
    [-1, 1],  # North East
    [0, 1],  # East
    [1, 1],  # South East
    [1, 0],  # South
    [1, -1],  # South West
    [0, -1],  # West
    [-1, -1],  # North West
]

COORD_REGEX = "[ \t]{0,}(-?[0-9]{1,3})[ \t]{0,},[ \t]{0,}(-?[0-9]{1,2})"


# array of BOARD_HEIGHT arrays, BOARD_WIDTH in length,
# representing the human player and computer
player_board: BoardType = []
computer_board: BoardType = []

# array representing the coordinates
# for each ship for player and computer
# array is in the same order as SHIPS
computer_ship_coords: List[List[CoordinateType]] = []


####################################
#
# SHOTS
#
# The number of shots computer/player
# has is determined by the shot "worth"
# of each ship the computer/player
# possesses. As long as the ship has one
# part not hit (i.e., ship was not
# sunk), the player gets all the shots
# from that ship.

# flag indicating if computer's shots are
# printed out during computer's turn
print_computer_shots = False

# keep track of the number
# of available computer shots
# inital shots are 7
num_computer_shots = 7

# keep track of the number
# of available player shots
# initial shots are 7
num_player_shots = 7

#
# SHOTS
#
####################################

# flag indicating whose turn it currently is
COMPUTER = False
PLAYER = True
active_turn = COMPUTER

####################
#
# game functions
#
####################

# random number functions
#
# seed the random number generator
random.seed()


# random_x_y
#


def random_x_y() -> CoordinateType:
    """Generate a valid x,y coordinate on the board"""

    x = random.randrange(1, BOARD_WIDTH + 1)
    y = random.randrange(1, BOARD_HEIGHT + 1)
    return (x, y)


def input_coord() -> CoordinateType:
    """
    Ask user for single (x,y) coordinate

    validate the coordinates are within the bounds
    of the board width and height. mimic the behavior
    of the original program which exited with error
    messages if coordinates where outside of array bounds.
    if input is not numeric, print error out to user and
    let them try again.
    """
    match = None
    while not match:
        coords = input("? ")
        match = re.match(COORD_REGEX, coords)
        if not match:
            print("!NUMBER EXPECTED - RETRY INPUT LINE")
    x = int(match.group(1))
    y = int(match.group(2))

    if x > BOARD_HEIGHT or y > BOARD_WIDTH:
        print("!OUT OF ARRAY BOUNDS IN LINE 1540")
        exit()

    if x <= 0 or y <= 0:
        print("!NEGATIVE ARRAY DIM IN LINE 1540")
        exit()

    return x, y


def generate_ship_coordinates(ship: int) -> List[CoordinateType]:
    """
    given a ship from the SHIPS array, generate
    the coordinates of the ship. the starting point
    of the ship's first coordinate is generated randomly.
    once the starting coordinates are determined, the
    possible directions of the ship, accounting for the
    edges of the board, are determined. once possible
    directions are found, a direction is randomly
    determined and the remaining coordinates are
    generated by adding or substraction from the starting
    coordinates as determined by direction.

    arguments:
      ship - index into the SHIPS array

    returns:
      array of sets of coordinates (x,y)
    """
    # randomly generate starting x,y coordinates
    start_x, start_y = random_x_y()

    # using starting coordinates and the ship type,
    # generate a vector of possible directions the ship
    # could be placed. directions are numbered 0-7 along
    # points of the compass (N, NE, E, SE, S, SW, W, NW)
    # clockwise. a vector of valid directions where the
    # ship does not go off the board is determined
    ship_len = SHIPS[ship][1] - 1
    dirs = [False for _ in range(8)]
    dirs[0] = (start_x - ship_len) >= 1
    dirs[2] = (start_y + ship_len) <= BOARD_WIDTH
    dirs[1] = dirs[0] and dirs[2]
    dirs[4] = (start_x + ship_len) <= BOARD_HEIGHT
    dirs[3] = dirs[2] and dirs[4]
    dirs[6] = (start_y - ship_len) >= 1
    dirs[5] = dirs[4] and dirs[6]
    dirs[7] = dirs[6] and dirs[0]
    directions = [p for p in range(len(dirs)) if dirs[p]]

    # using the vector of valid directions, pick a
    # random direction to place the ship
    dir_idx = random.randrange(len(directions))
    direction = directions[dir_idx]

    # using the starting x,y, direction and ship
    # type, return the coordinates of each point
    # of the ship. VALID_MOVES is a staic array
    # of coordinate offsets to walk from starting
    # coordinate to the end coordinate in the
    # chosen direction
    ship_len = SHIPS[ship][1] - 1
    d_x = VALID_MOVES[direction][0]
    d_y = VALID_MOVES[direction][1]

    coords = [(start_x, start_y)]
    x_coord = start_x
    y_coord = start_y
    for _ in range(ship_len):
        x_coord = x_coord + d_x
        y_coord = y_coord + d_y
        coords.append((x_coord, y_coord))
    return coords


def create_blank_board() -> BoardType:
    """Create a blank game board"""
    return [[None for _y in range(BOARD_WIDTH)] for _x in range(BOARD_HEIGHT)]


def print_board(board: BoardType) -> None:
    """Print out the game board for testing purposes"""
    # print board header (column numbers)
    print("  ", end="")
    for z in range(BOARD_WIDTH):
        print(f"{z+1:3}", end="")
    print()

    for x in range(len(board)):
        print(f"{x+1:2}", end="")
        for y in range(len(board[x])):
            if board[x][y] is None:
                print(f"{' ':3}", end="")
            else:
                print(f"{board[x][y]:3}", end="")
        print()


def place_ship(board: BoardType, coords: List[CoordinateType], ship: int) -> None:
    """
    Place a ship on a given board.

    updates
    the board's row,column value at the given
    coordinates to indicate where a ship is
    on the board.

    inputs: board - array of BOARD_HEIGHT by BOARD_WIDTH
            coords - array of sets of (x,y) coordinates of each
                     part of the given ship
            ship - integer representing the type of ship (given in SHIPS)
    """
    for coord in coords:
        board[coord[0] - 1][coord[1] - 1] = ship


def generate_board() -> Tuple[BoardType, List[List[CoordinateType]]]:
    """
    NOTE: A little quirk that exists here and in the orginal
          game: Ships are allowed to cross each other!
          For example: 2 destroyers, length 2, one at
          [(1,1),(2,2)] and other at [(2,1),(1,2)]
    """
    board = create_blank_board()

    ship_coords = []
    for ship in range(len(SHIPS)):
        placed = False
        coords = []
        while not placed:
            coords = generate_ship_coordinates(ship)
            clear = all(board[coord[0] - 1][coord[1] - 1] is None for coord in coords)
            if clear:
                placed = True
        place_ship(board, coords, ship)
        ship_coords.append(coords)
    return board, ship_coords


def execute_shot(
    turn: bool, board: BoardType, x: int, y: int, current_turn: int
) -> int:
    """
    given a board and x, y coordinates,
    execute a shot. returns True if the shot
    is valid, False if not
    """
    square = board[x - 1][y - 1]
    ship_hit = -1
    if square is not None and square >= 0 and square < len(SHIPS):
        ship_hit = square
    board[x - 1][y - 1] = 10 + current_turn
    return ship_hit


def calculate_shots(board: BoardType) -> int:
    """Examine each board and determine how many shots remaining"""
    ships_found = [0 for _ in range(len(SHIPS))]
    for x in range(BOARD_HEIGHT):
        for y in range(BOARD_WIDTH):
            square = board[x - 1][y - 1]
            if square is not None and square >= 0 and square < len(SHIPS):
                ships_found[square] = 1
    return sum(
        SHIPS[ship][2]
        for ship in range(len(ships_found))
        if ships_found[ship] == 1
    )


def initialize_game() -> None:
    # initialize the global player and computer boards
    global player_board
    player_board = create_blank_board()

    # generate the ships for the computer's board
    global computer_board
    global computer_ship_coords
    computer_board, computer_ship_coords = generate_board()

    # print out the title 'screen'
    print("{:>38}".format("SALVO"))
    print("{:>57s}".format("CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"))
    print()
    print("{:>52s}".format("ORIGINAL BY LAWRENCE SIEGEL, 1973"))
    print("{:>56s}".format("PYTHON 3 PORT BY TODD KAISER, MARCH 2021"))
    print("\n")

    # ask the player for ship coordinates
    print("ENTER COORDINATES FOR...")
    ship_coords = []
    for ship in SHIPS:
        print(ship[0])
        list = []
        for _ in range(ship[1]):
            x, y = input_coord()
            list.append((x, y))
        ship_coords.append(list)

    # add ships to the user's board
    for ship_index in range(len(SHIPS)):
        place_ship(player_board, ship_coords[ship_index], ship_index)

    # see if the player wants the computer's ship
    # locations printed out and if the player wants to
    # start
    input_loop = True
    player_start = "YES"
    while input_loop:
        player_start = input("DO YOU WANT TO START? ")
        if player_start == "WHERE ARE YOUR SHIPS?":
            for ship_index in range(len(SHIPS)):
                print(SHIPS[ship_index][0])
                coords = computer_ship_coords[ship_index]
                for coord in coords:
                    x = coord[0]
                    y = coord[1]
                    print(f"{x:2}", f"{y:2}")
        else:
            input_loop = False

    # ask the player if they want the computer's shots
    # printed out each turn
    global print_computer_shots
    see_computer_shots = input("DO YOU WANT TO SEE MY SHOTS? ")
    if see_computer_shots.lower() == "yes":
        print_computer_shots = True

    global first_turn
    if player_start.lower() != "yes":
        first_turn = COMPUTER

    # calculate the initial number of shots for each
    global num_computer_shots, num_player_shots
    num_player_shots = calculate_shots(player_board)
    num_computer_shots = calculate_shots(computer_board)


####################################
#
# Turn Control
#
# define functions for executing the turns for
# the player and the computer. By defining this as
# functions, we can easily start the game with
# either computer or player and alternate back and
# forth, replicating the gotos in the original game


# initialize the first_turn function to the player's turn
first_turn = PLAYER


def execute_turn(turn: bool, current_turn: int) -> int:
    global num_computer_shots, num_player_shots

    # print out the number of shots the current player has
    board = None
    num_shots = 0
    if turn == COMPUTER:
        print(f"I HAVE {num_computer_shots} SHOTS.")
        board = player_board
        num_shots = num_computer_shots
    else:
        print(f"YOU HAVE {num_player_shots} SHOTS.")
        board = computer_board
        num_shots = num_player_shots

    shots = []
    for _shot in range(num_shots):
        valid_shot = False
        x = -1
        y = -1

        # loop until we have a valid shot. for the
        # computer, we randomly pick a shot. for the
        # player we request shots
        while not valid_shot:
            x, y = random_x_y() if turn == COMPUTER else input_coord()
            square = board[x - 1][y - 1]
            if square is not None and square > 10:
                if turn == PLAYER:
                    print("YOU SHOT THERE BEFORE ON TURN", square - 10)
                continue
            shots.append((x, y))
            valid_shot = True

    hits = []
    for shot in shots:
        hit = execute_shot(turn, board, shot[0], shot[1], current_turn)
        if hit >= 0:
            hits.append(hit)
        if turn == COMPUTER and print_computer_shots:
            print(shot[0], shot[1])

    for hit in hits:
        if turn == COMPUTER:
            print("I HIT YOUR", SHIPS[hit][0])
        else:
            print("YOU HIT MY", SHIPS[hit][0])

    if turn == COMPUTER:
        num_player_shots = calculate_shots(board)
        return num_player_shots
    else:
        num_computer_shots = calculate_shots(board)
        return num_computer_shots


#
# Turn Control
#
######################################


def main() -> None:
    current_turn = 0
    initialize_game()

    # execute turns until someone wins or we run
    # out of squares to shoot
    game_over = False
    while not game_over:
        current_turn += 1

        print("\n")
        print("TURN", current_turn)

        if (
            execute_turn(first_turn, current_turn) == 0
            or execute_turn(not first_turn, current_turn) == 0
        ):
            game_over = True
            continue


if __name__ == "__main__":
    main()
